package com.stars.easyms.redis.lock;

import com.stars.easyms.redis.exception.RedisRuntimeException;
import com.stars.easyms.redis.template.EasyMsRedisTemplate;
import com.stars.easyms.redis.exception.DistributedLockTimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.script.RedisScript;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

/**
 * 分布式锁
 *
 * @author guoguifang
 * @date 2018-04-23 13:54
 * @since 1.0.0
 */
@Slf4j
public final class DistributedLock {

    private static EasyMsRedisTemplate easyMsRedisTemplate;

    private final String redisKey;

    private int expireSecond;

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    private static final RedisScript REDIS_SCRIPT = RedisScript.of("local rs=redis.pcall('setnx',KEYS[1],ARGV[1]);if(rs<1) then return 'F';end;redis.pcall('expire',KEYS[1],tonumber(ARGV[2]));return 'S';", String.class);

    private static final String DEFAULT_DISTRIBUTED_LOCK_PREFIX = "DistributedLock";

    public static final int DEFAULT_EXPIRE_SECOND = 60;

    public static final int DEFAULT_RETRY_INTERVAL = 100;

    /**
     * 创建一个分布式锁
     *
     * @param lockName 锁名称
     */
    public DistributedLock(String lockName) {
        this(lockName, null, DEFAULT_EXPIRE_SECOND);
    }

    /**
     * 创建一个分布式锁
     *
     * @param lockName 锁名称
     * @param item     子项
     */
    public DistributedLock(String lockName, String item) {
        this(lockName, item, DEFAULT_EXPIRE_SECOND);
    }

    /**
     * 创建一个分布式锁
     *
     * @param lockName     锁名称
     * @param expireSecond 锁失效时间(单位：秒)
     */
    public DistributedLock(String lockName, int expireSecond) {
        this(lockName, null, expireSecond);
    }

    /**
     * 创建一个分布式锁
     *
     * @param lockName     锁名称
     * @param item         子项
     * @param expireSecond 锁失效时间(单位：秒)
     */
    public DistributedLock(String lockName, String item, int expireSecond) {
        if (StringUtils.isBlank(lockName)) {
            throw new IllegalArgumentException("DistributedLock lockName can't be blank!");
        }
        this.redisKey = getEasyMsRedisTemplate().getRedisKeyWithPrefix(DEFAULT_DISTRIBUTED_LOCK_PREFIX, lockName, item);
        this.expireSecond = expireSecond <= 0 ? DEFAULT_EXPIRE_SECOND : expireSecond;
    }

    /**
     * 单次加锁
     *
     * @return 是否加锁成功：true 成功,false 失败
     */
    public boolean lock() {
        return getLock(true);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时(默认加锁失败后重试间隔为100毫秒)
     *
     * @param timeout 超时时间(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public void blockLock(long timeout) throws DistributedLockTimeoutException {
        blockLock(timeout, DEFAULT_RETRY_INTERVAL);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时
     *
     * @param timeout       超时时间(单位：毫秒)
     * @param retryInterval 加锁失败后重试间隔(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public void blockLock(long timeout, long retryInterval) throws DistributedLockTimeoutException {
        if (timeout < 0 || retryInterval < 0) {
            throw new IllegalArgumentException("Argument blockMilliseconds or retryIntervalMilliseconds is invalid!");
        }
        long endTime = System.currentTimeMillis() + timeout;
        do {
            if (getLock(false)) {
                return;
            }
            try {
                Thread.sleep(retryInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } while (System.currentTimeMillis() < endTime);
        throw new DistributedLockTimeoutException("Get redis distributedLock {} time out!", redisKey);
    }

    /**
     * 解锁
     */
    public void unlock() {
        getEasyMsRedisTemplate().delete(redisKey);
    }

    /**
     * 单次加锁(使用默认的过期时间：60秒)
     *
     * @param lockName 锁名称
     * @return
     */
    public static boolean tryLock(String lockName) {
        return tryLock(lockName, null, DEFAULT_EXPIRE_SECOND);
    }

    /**
     * 单次加锁(使用默认的过期时间：60秒)
     *
     * @param lockName 锁名称
     * @param item     子项
     * @return
     */
    public static boolean tryLock(String lockName, String item) {
        return tryLock(lockName, item, DEFAULT_EXPIRE_SECOND);
    }

    /**
     * 单次加锁
     *
     * @param lockName     锁名称
     * @param expireSecond 锁失效时间(单位：秒)
     * @return
     */
    public static boolean tryLock(String lockName, int expireSecond) {
        return tryLock(lockName, null, expireSecond);
    }

    /**
     * 单次加锁
     *
     * @param lockName     锁名称
     * @param item         子项
     * @param expireSecond 锁失效时间(单位：秒)
     * @return
     */
    public static boolean tryLock(String lockName, String item, int expireSecond) {
        if (StringUtils.isBlank(lockName)) {
            throw new IllegalArgumentException("DistributedLock lockName can't be blank!");
        }
        return getLock(getEasyMsRedisTemplate().getRedisKeyWithPrefix(DEFAULT_DISTRIBUTED_LOCK_PREFIX, lockName, item), expireSecond <= 0 ? DEFAULT_EXPIRE_SECOND : expireSecond, true);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时(默认加锁失败后重试间隔为100毫秒)
     *
     * @param lockName 锁名称
     * @param timeout  超时时间(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, long timeout) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, null, DEFAULT_EXPIRE_SECOND, timeout, DEFAULT_RETRY_INTERVAL);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时(默认加锁失败后重试间隔为100毫秒)
     *
     * @param lockName     锁名称
     * @param expireSecond 锁失效时间(单位：秒)
     * @param timeout      超时时间(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, int expireSecond, long timeout) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, null, expireSecond, timeout, DEFAULT_RETRY_INTERVAL);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时(默认加锁失败后重试间隔为100毫秒)
     *
     * @param lockName 锁名称
     * @param item     子项
     * @param timeout  超时时间(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, String item, long timeout) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, item, DEFAULT_EXPIRE_SECOND, timeout, DEFAULT_RETRY_INTERVAL);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时(默认加锁失败后重试间隔为100毫秒)
     *
     * @param lockName     锁名称
     * @param item         子项
     * @param expireSecond 锁失效时间(单位：秒)
     * @param timeout      超时时间(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, String item, int expireSecond, long timeout) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, item, expireSecond, timeout, DEFAULT_RETRY_INTERVAL);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时
     *
     * @param lockName      锁名称
     * @param timeout       超时时间(单位：毫秒)
     * @param retryInterval 加锁失败后重试间隔(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, long timeout, long retryInterval) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, null, DEFAULT_EXPIRE_SECOND, timeout, retryInterval);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时
     *
     * @param lockName      锁名称
     * @param expireSecond  锁失效时间(单位：秒)
     * @param timeout       超时时间(单位：毫秒)
     * @param retryInterval 加锁失败后重试间隔(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, int expireSecond, long timeout, long retryInterval) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, null, expireSecond, timeout, retryInterval);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时
     *
     * @param lockName      锁名称
     * @param item          子项
     * @param timeout       超时时间(单位：毫秒)
     * @param retryInterval 加锁失败后重试间隔(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, String item, long timeout, long retryInterval) throws DistributedLockTimeoutException {
        tryBlockLock(lockName, item, DEFAULT_EXPIRE_SECOND, timeout, retryInterval);
    }

    /**
     * 阻塞式加锁，即如果加锁失败，则循环重试，直到加锁成功或者超时
     *
     * @param lockName      锁名称
     * @param item          子项
     * @param expireSecond  锁失效时间(单位：秒)
     * @param timeout       超时时间(单位：毫秒)
     * @param retryInterval 加锁失败后重试间隔(单位：毫秒)
     * @throws DistributedLockTimeoutException 分布式锁超时异常
     */
    public static void tryBlockLock(String lockName, String item, int expireSecond, long timeout, long retryInterval) throws DistributedLockTimeoutException {
        if (timeout < 0 || retryInterval < 0) {
            throw new IllegalArgumentException("blockMilliseconds or retryIntervalMilliseconds is invalid!");
        }
        String redisKey = getEasyMsRedisTemplate().getRedisKeyWithPrefix(DEFAULT_DISTRIBUTED_LOCK_PREFIX, lockName, item);
        long endTime = System.currentTimeMillis() + timeout;
        do {
            if (getLock(redisKey, expireSecond <= 0 ? DEFAULT_EXPIRE_SECOND : expireSecond, false)) {
                return;
            }
            try {
                Thread.sleep(retryInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } while (System.currentTimeMillis() < endTime);
        throw new DistributedLockTimeoutException("Get redis distributedLock {} time out!", redisKey);
    }

    /**
     * 解锁
     *
     * @param lockName 锁名称
     */
    public static void unlock(String lockName) {
        unlock(lockName, null);
    }

    /**
     * 解锁
     *
     * @param lockName 锁名称
     * @param item     子项
     */
    public static void unlock(String lockName, String item) {
        getEasyMsRedisTemplate().delete(getEasyMsRedisTemplate().getRedisKeyWithPrefix(DEFAULT_DISTRIBUTED_LOCK_PREFIX, lockName, item));
    }

    @SuppressWarnings("unchecked")
    private static boolean getLock(String redisKey, int expireSecond, boolean isRecordFailLog) {
        List<String> argList = new ArrayList<>();
        argList.add(DATE_TIME_FORMATTER.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault())));
        argList.add(String.valueOf(expireSecond));
        List<String> keyList = new ArrayList<>();
        keyList.add(redisKey);
        String result = getEasyMsRedisTemplate().execute(REDIS_SCRIPT, keyList, argList);
        if ("S".equals(result)) {
            if (log.isDebugEnabled()) {
                log.debug("Redis distributedLock ({}) lock success, expire time {}s!", redisKey, expireSecond);
            }
            return true;
        }
        if (isRecordFailLog && log.isDebugEnabled()) {
            log.debug("Redis distributedLock ({}) lock failure!", redisKey);
        }
        return false;
    }

    private boolean getLock(boolean isRecordFailLog) {
        return getLock(redisKey, expireSecond, isRecordFailLog);
    }

    public static void setEasyMsRedisTemplate(EasyMsRedisTemplate easyMsRedisTemplate) {
        if (DistributedLock.easyMsRedisTemplate == null) {
            DistributedLock.easyMsRedisTemplate = easyMsRedisTemplate;
        }
    }

    public static EasyMsRedisTemplate getEasyMsRedisTemplate() {
        if (easyMsRedisTemplate == null) {
            throw new RedisRuntimeException("Redis is disabled, cannot use redis distributed locks");
        }
        return easyMsRedisTemplate;
    }
}
